今天做一個可以實際用的碼錶工具:開始、暫停、重置、圈速紀錄(顯示單圈與累計),並支援快捷鍵。程式完全使用 Python 標準庫(Tkinter + time.perf_counter),不需要安裝額外套件。
成品功能
開始/暫停/重置
圈速(Lap):每次按下 Lap,記錄「單圈耗時」與「累計時間」
快捷鍵:Space=開始或暫停、L=圈速、R=重置
較精準的計時:用 time.perf_counter()(高解析度、適合量測時間)
UI 更新頻率約 30 FPS,讀秒滑順但不吃太多資源
Tkinter 為標準庫。Windows 與 macOS 內建;部分 Linux 需先安裝 python3-tk。
安裝與環境
Python 3.8 以上即可
無需額外安裝套件
Windows / macOS / Linux 皆可
完整程式碼(存成 stopwatch_gui.py)
import tkinter as tk
from tkinter import ttk
import time
# ---------- 工具:格式化時間 ----------
def fmt(t: float) -> str:
# 轉成 HH:MM:SS.cc(cc=百分秒)
t = max(0.0, t)
h = int(t // 3600); t -= h * 3600
m = int(t // 60); t -= m * 60
s = int(t)
cs = int(round((t - s) * 100))
if cs == 100: s, cs = s + 1, 0
return (f"{h:02d}:{m:02d}:{s:02d}.{cs:02d}") if h > 0 else (f"{m:02d}:{s:02d}.{cs:02d}")
# ---------- 計時狀態 ----------
running = False # 是否在跑
start_t = 0.0 # 本次開始的 perf_counter
accum = 0.0 # 暫停前的累計秒數
laps = [] # 每一圈的「累計秒數」
tick_job = None # after 回呼 id
def now_elapsed() -> float:
"""目前累計秒數(暫停時固定,計時時以 perf_counter 動態累加)"""
return accum + (time.perf_counter() - start_t if running else 0.0)
# ---------- 控制邏輯 ----------
def toggle_start():
global running, start_t
if running:
pause()
else:
running = True
start_t = time.perf_counter()
status_var.set("計時中")
tick()
def pause():
global running, accum, tick_job
if not running: return
accum = now_elapsed()
running = False
status_var.set("已暫停")
if tick_job:
root.after_cancel(tick_job)
def reset():
global running, accum, laps, tick_job
if tick_job:
root.after_cancel(tick_job)
running = False
accum = 0.0
laps.clear()
time_var.set("00:00.00")
total_var.set("圈速:0")
status_var.set("就緒")
lap_list.delete(0, tk.END)
def add_lap():
"""新增一筆圈速:顯示單圈耗時與累計時間(最新圈顯示在最上方)"""
if not running and accum == 0.0:
return
total = now_elapsed()
laps.append(total)
idx = len(laps)
lap_time = total if idx == 1 else (total - laps[-2])
lap_list.insert(0, f"Lap {idx:02d} 單圈 {fmt(lap_time)} 累計 {fmt(total)}")
total_var.set(f"圈速:{idx}")
def tick():
"""每 33ms 更新一次 UI(約 30fps)"""
global tick_job
if not running: return
time_var.set(fmt(now_elapsed()))
tick_job = root.after(33, tick)
# ---------- GUI ----------
root = tk.Tk()
root.title("Stopwatch / Lap")
main = ttk.Frame(root, padding=16); main.grid()
status_var = tk.StringVar(value="就緒")
time_var = tk.StringVar(value="00:00.00")
total_var = tk.StringVar(value="圈速:0")
ttk.Label(main, textvariable=status_var).grid(row=0, column=0, sticky="w")
ttk.Label(main, textvariable=time_var, font=("Segoe UI", 36)).grid(row=1, column=0, columnspan=3, pady=6)
btns = ttk.Frame(main); btns.grid(row=2, column=0, columnspan=3, pady=8)
ttk.Button(btns, text="開始 / 暫停 (Space)", command=toggle_start).grid(row=0, column=0, padx=4)
ttk.Button(btns, text="圈速 (L)", command=add_lap).grid(row=0, column=1, padx=4)
ttk.Button(btns, text="重置 (R)", command=reset).grid(row=0, column=2, padx=4)
# 圈速清單(最新在最上方)
list_frame = ttk.Frame(main); list_frame.grid(row=3, column=0, columnspan=3, sticky="nsew")
root.rowconfigure(3, weight=1); root.columnconfigure(0, weight=1)
list_frame.rowconfigure(0, weight=1); list_frame.columnconfigure(0, weight=1)
lap_list = tk.Listbox(list_frame, height=8)
lap_list.grid(row=0, column=0, sticky="nsew")
scroll = ttk.Scrollbar(list_frame, orient="vertical", command=lap_list.yview)
scroll.grid(row=0, column=1, sticky="ns")
lap_list.configure(yscrollcommand=scroll.set)
ttk.Label(main, textvariable=total_var).grid(row=4, column=0, sticky="w", pady=(6,0))
# 快捷鍵
root.bind("<space>", lambda e: toggle_start())
root.bind("<l>", lambda e: add_lap()); root.bind("<L>", lambda e: add_lap())
root.bind("<r>", lambda e: reset()); root.bind("<R>", lambda e: reset())
root.mainloop()
實作:
實作重點解說
1)為什麼用 time.perf_counter()?
time.time() 會受系統時鐘調整影響,不適合做碼錶
time.perf_counter() 提供單調遞增、高解析度的計時來源,更準確
設計上不以「每秒 +1」來計時,而是讀取起點到現在的差值。因此即使 UI 更新有抖動,累計時間仍準確。
2)避免「定時器越跑越快」:記得取消 after
Tkinter 的 after 會排程下一次回呼;若在暫停或重置時沒取消,就可能疊出多條計時線。
程式在 pause()、reset() 皆會呼叫 root.after_cancel(tick_job),確保只留下唯一的定時器。
3)圈速(Lap)的計算方式
laps 儲存每次按下 Lap 時的累計秒數
單圈耗時 = 本次累計 − 上次累計
第一圈沒有上一次,直接等於本次累計
新增在 Listbox 的最上方,最新一圈容易查看
4)UI 更新頻率 33ms(約 30 FPS)
root.after(33, tick) 讓介面更新流暢,同時避免 10ms 這種過度頻繁造成 CPU 浪費
文字採「百分秒(0.01s)」顯示;就算更新頻率略抖,總秒數的計算仍由 perf_counter() 決定,不會偏差
常見問題與排錯
按開始沒反應? 檢查是否有輸入焦點在 Listbox;按一下視窗空白處或按 Space 再試。
時間顯示跳動不均? UI 更新受系統排程影響很正常;總時間不受影響。
電腦睡眠/休眠後誤差? 喚醒後 perf_counter() 會繼續走,碼錶視為「不中斷」;若要忽略睡眠時段,可在喚醒後按一次暫停再開始。
下一篇補充可以新增的功能~